/*
 * #%L
 * Osm2garminAPI
 * %%
 * Copyright (C) 2011 Frantisek Mantlik <frantisek at mantlik.cz>
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, either version 2 of the 
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public 
 * License along with this program.  If not, see
 * <http://www.gnu.org/licenses/gpl-2.0.html>.
 * #L%
 */
package be.dch.srtm2osm;

import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.GZIPOutputStream;

import org.apache.log4j.Logger;

import uk.me.parabola.splitter.Area;
import be.dch.Config;
import be.dch.gui.panels.Srtm2OsmPanel;
import be.dch.srtm2osm.domain.Contour;
import be.dch.srtm2osm.domain.Contours;
import be.dch.srtm2osm.domain.Point;

/**
 * 
 * @author fm
 */
public final class Srtm2Osm {

    private static final Logger LOG = Logger.getLogger(Srtm2Osm.class);

    private final transient Map<Point, Integer> starts = new HashMap<Point, Integer>();
    private final transient Map<Point, Integer> ends = new HashMap<Point, Integer>();

    private final transient int lat, lon;
    private final transient String outputFile;
    public transient boolean allSrtms = true;
    private final transient Srtm2OsmConfig config;

    /**
     * 
     * @param area
     * @param outputFile
     */
    public Srtm2Osm(final Area area, final String outputFile) {
	this.config = Config.getInstance().getSrtm2OsmConfig();
	this.lat = area.getMinLat();
	this.lon = area.getMinLong();
	this.outputFile = outputFile;
    }

    public void run() {
	final List<Contour> contours = new ArrayList<Contour>();
	final int srtmStep = this.config.getStep();
	final double offsLat = this.config.getCorrLat();
	final double offsLon = this.config.getCorrLng();
	final int minorInterval = this.config.getMinorInterval();
	final int mediumInterval = this.config.getMediumInterval();
	final int majorInterval = this.config.getMajorInterval();
	final int plotMinorThreshold = this.config.getPlotMinorThreshold();
	final int plotMediumThreshold = this.config.getPlotMediumThreshold();
	final int contoursDensity = this.config.getContoursDensity();

	String coords;
	final List<Contour> gridcont = new ArrayList<Contour>();
	for (int la = 0; la < srtmStep; la++) {
	    for (int lo = 0; lo < srtmStep; lo++) {
		coords = Math.abs(this.lat + la) + ((this.lat + la) > 0 ? "N " : "S ") + Math.abs(this.lon + lo) + ((this.lon + lo) > 0 ? "E" : "W");
		Srtm srtm = null;
		if (Srtm.exists(this.lon + lo, this.lat + la)) {
		    int i = 0;
		    while ((srtm == null) && (i <= 5)) {
			i++;
			this.setStatus("Contours " + coords + ": Downloading SRTM data - attempt no. " + i);
			srtm = Srtm.get(this.lon + lo, this.lat + la);
			if (srtm == null) {
			    try {
				Thread.sleep(1000);
			    } catch (final InterruptedException ex) {
			    }
			}
		    }
		    if (srtm == null) {
			this.allSrtms = false;
		    }
		}
		if (srtm == null) {
		    this.setStatus("Contours " + coords + ": No SRTM data.");
		    continue;
		}
		this.setStatus("Contours " + coords + ": Preparing data.");
		final float[][] data = new float[121][121];
		this.setStatus("Contours " + coords + ": Making contours.");
		try {
		    gridcont.clear();
		    for (int i = 0; i < 10; i++) {
			for (int j = 0; j < 10; j++) {
			    int mindata = Integer.MAX_VALUE;
			    int maxdata = Integer.MIN_VALUE;
			    for (int ii = 0; ii < 121; ii++) {
				for (int jj = 0; jj < 121; jj++) {
				    final int dd = srtm.data[(i * 120) + ii][(j * 120) + jj];
				    if ((dd > -10000) && (dd < 10000)) {
					mindata = Math.min(mindata, dd);
					maxdata = Math.max(maxdata, dd);
					data[ii][jj] = dd;
				    } else {
					data[ii][jj] = 32768.0f;
				    }
				}
			    }
			    final int extent = maxdata - mindata;
			    int interval = extent < plotMinorThreshold ? minorInterval : mediumInterval;
			    interval = extent < plotMediumThreshold ? interval : majorInterval;
			    if (extent < (2 * interval)) {
				if (extent > 15) {
				    interval = 10;
				} else if (extent > 10) {
				    interval = 5;
				} else if (extent > 5) {
				    interval = 2;
				} else {
				    interval = 1;
				}
			    }
			    final Contours contoursMaker = new Contours(data, 121, 121, ((1.0d * this.lat) + la + (i / 10d)) - offsLat, ((1.0d * this.lon) + lo + (j / 10d)) - offsLon, 1d / 1200d, interval, 32768.0d);
			    final List<Contour> c = contoursMaker.makeContours();
			    this.addContours(gridcont, c, null);
			    this.setStatus("Contours " + coords + ": Making contours - " + ((10 * i) + j) + " %");
			}
		    }
		    this.setStatus("Contours " + coords + ": Checking contours density.");
		    this.checkContoursDensity(gridcont, 1201, 1201, ((1.0d * this.lat) + la) - offsLat, ((1.0d * this.lon) + lo) - offsLon, 1d / 1200d, contoursDensity, majorInterval);
		    final String prefix = "Contours " + coords + ": Joining contours " + gridcont.size() + "->" + contours.size() + " ";
		    this.addContours(contours, gridcont, prefix);
		} catch (final Exception ex) {
		    Srtm2Osm.LOG.error(ex);
		    this.setStatus("Contours " + coords + ": Contours creation failed.");
		    continue;
		}
	    }
	}
	coords = Math.abs(this.lat) + (this.lat > 0 ? "N " : "S ") + Math.abs(this.lon) + (this.lon > 0 ? "E" : "W");
	if ((contours == null) || contours.isEmpty()) {
	    this.setStatus("Contours " + coords + ": No contours created.");
	    synchronized (this) {
		this.notifyAll();
	    }
	    return;
	}
	// export contours to file
	this.setStatus("Contours " + coords + ": Creating OSM file.");
	PrintStream ss = null;
	try {
	    ss = new PrintStream(new GZIPOutputStream(new BufferedOutputStream(new FileOutputStream(this.outputFile))));
	    ss.println("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
	    ss.println("<osm version=\"0.5\" generator=\"Srtm2Osm.java\">");
	    for (int pass = 0; pass < 2; pass++) {
		long id = this.config.getContourStartId();
		long wayId = this.config.getContourStartWayId();
		final boolean nodes = pass == 0;
		final boolean way = pass == 1;
		for (int i = 0; i < contours.size(); i++) {
		    final Contour contour = contours.get(i);
		    final int level = (int) contour.z;
		    final boolean major = (level % majorInterval) == 0;
		    contour.outputOsm(wayId, id, major, ss, nodes, way);
		    wayId++;
		    id += contour.data.size();
		    this.setStatus("Contours " + coords + ": Creating OSM file - " + (int) (((50.0 * i) / contours.size()) + (50.0 * pass)) + " %");
		}
	    }
	    ss.println("</osm>");
	} catch (final IOException ex) {
	    Srtm2Osm.LOG.error(ex);
	    this.setStatus("Error creating " + this.outputFile);

	} finally {
	    ss.close();
	}
	synchronized (this) {
	    this.notifyAll();
	}
    }

    private void setStatus(final String string) {
	Srtm2Osm.LOG.info(string);
	Srtm2OsmPanel.getInstance().getTxtArea().append(string);

    }

    /*
     * Add contours from list add to list contours, merge where possible
     */
    private void addContours(final List<Contour> contours, final List<Contour> add, final String logPrefix) {
	for (int i = 0; i < contours.size(); i++) {
	    final Contour c = contours.get(i);
	    if (c.isClosed() || (c.getData().size() < 2)) {
		continue;
	    }
	    this.starts.put(c.getData().get(0), i);
	    this.ends.put(c.getData().get(c.getData().size() - 1), i);
	}
	for (int i = 0; i < add.size(); i++) {
	    final Contour c = add.get(i);
	    if (c.getData().size() < 2) {
		continue;
	    }
	    if (contours.isEmpty() || c.isClosed()) {
		contours.add(c);
		continue;
	    }
	    boolean finished = false;
	    Contour cc = null;
	    final Point newstart = c.getData().get(0);
	    final Point newend = c.getData().get(c.getData().size() - 1);
	    if ((newstart == null) || (newend == null)) {
		continue;
	    }
	    if (this.starts.containsKey(newend)) {
		cc = contours.get(this.starts.get(newend));
	    } else if (this.ends.containsKey(newstart)) {
		cc = contours.get(this.ends.get(newstart));
	    }
	    if ((cc != null) && (!cc.isClosed())) {
		final Point start = cc.getData().get(0);
		final Point end = cc.getData().get(cc.getData().size() - 1);
		if (end.equals(newstart)) {
		    int j;
		    if (this.ends.containsKey(newstart)) {
			j = this.ends.remove(newstart);
			this.ends.put(newend, j);
		    }
		    j = contours.indexOf(cc);
		    contours.remove(cc);
		    contours.add(j, this.joinContours(cc, c));
		    finished = true;
		} else if (newend.equals(start)) {
		    int j = this.starts.remove(newend);
		    this.starts.put(newstart, j);
		    j = contours.indexOf(cc);
		    contours.remove(cc);
		    contours.add(j, this.joinContours(c, cc));
		    finished = true;
		}
	    }
	    if (!finished) {
		contours.add(c);
		this.starts.put(newstart, contours.size() - 1);
		this.ends.put(newend, contours.size() - 1);
	    }
	    if (logPrefix != null) {
		this.setStatus(logPrefix + (int) ((100.0 * i) / add.size()) + " %");
	    }
	}
	this.starts.clear();
	this.ends.clear();
    }

    /*
     * join c2 to the end of c1 suppose the last point of c1 equals to the first
     * point of c2 (not checked)
     */
    private Contour joinContours(final Contour c1, final Contour c2) {
	final Point end = c1.getData().get(c1.getData().size() - 1);
	final Point start = c2.getData().get(0);
	if (!end.equals(start)) {
	    Srtm2Osm.LOG.error("Joining contours with non-equal ends: " + end + " and " + start);
	}
	for (int i = 1; i < c2.getData().size(); i++) {
	    c1.getData().add(c2.getData().get(i));
	}
	c1.setClosed(c1.getData().get(0).equals(c1.getData().get(c1.getData().size() - 1)));
	return c1;
    }

    /*
     * Delete segments when a cell contains more than contoursDensity segments;
     * keep major contours if they fit maximum density in a cell
     */
    private void checkContoursDensity(final List<Contour> contours, final int nlat, final int nlon, final double startlat, final double startlon, final double delta, final int contoursDensity, final int majorInterval) {
	if (contours.isEmpty()) {
	    return;
	}
	final int[][] density = new int[nlat][nlon];
	final int[][] majorDensity = new int[nlat][nlon];
	for (final Contour contour : contours) {
	    for (int i = 1; i < contour.getData().size(); i++) {
		final Point p1 = contour.getData().get(i - 1);
		final Point p2 = contour.getData().get(i);
		final double la = (p1.getX() + p2.getX()) / 2;
		final double lo = (p1.getY() + p2.getY()) / 2;
		final int ii = (int) ((la - startlat) / delta);
		final int jj = (int) ((lo - startlon) / delta);
		density[ii][jj]++;
		if ((((int) contour.getZ()) % majorInterval) == 0) {
		    majorDensity[ii][jj]++;
		}
	    }
	}
	for (int k = 0; k < contours.size(); k++) {
	    final Contour contour = contours.get(k);
	    for (int i = 1; i < contour.getData().size(); i++) {
		final Point p1 = contour.getData().get(i - 1);
		final Point p2 = contour.getData().get(i);
		final double la = (p1.getX() + p2.getX()) / 2;
		final double lo = (p1.getY() + p2.getY()) / 2;
		final int ii = (int) ((la - startlat) / delta);
		final int jj = (int) ((lo - startlon) / delta);
		if ((majorDensity[ii][jj] > contoursDensity) || ((density[ii][jj] > contoursDensity) && ((contour.getZ() % majorInterval) != 0))) {
		    // remove segment from contour
		    if (i == 1) { // first segment, delete first point
			contour.getData().remove(0);
			i = i - 1; // next segment replaces deleted one -
				   // recheck
			contour.setClosed(false);
		    } else if (i == (contour.getData().size() - 1)) { // last
								      // segment,
								      // delete
								      // last
								      // point
			contour.getData().remove(i);
			contour.setClosed(false);
		    } else if (contour.isClosed()) {
			final int id = 1;
			while (id < i) {
			    contour.getData().add(contour.getData().remove(0));
			    contour.setClosed(false);
			    i = 0;
			}
		    } else {
			// middle segment - break contour
			final Contour newContour = new Contour();
			newContour.setZ(contour.getZ());
			while (contour.getData().size() > i) {
			    newContour.getData().add(contour.getData().remove(i));
			}
			contours.add(newContour);
		    }
		}
	    }
	}
    }
}
